<?php

/***
 * Class AclClass provides full access control list functionality to any application that implements it.
 ***/
class AclClass
{
   /**
	* A multidimentional array of roles
	* @access Private
	* @var Array
	*/
	private $roles;


   /**
	* An array of actions with assigned roles
	* @access Private
	* @var Array
	*/
	private $actions;


   // ! Constructor Method

   /**
	* Instantiates class and defines instance variables.
	*
	* @param none
	* @author Daniel Wilhelm II Murdoch <wilhelm.murdoch@gmail.com>
	* @since v 1.0.0
	* @access public
	* @return Void
	*/
	public function __construct()
	{
		$this->roles = array();
		$this->actions = array();
	}


   // ! Mutator Method

   /**
	* Adds a new role to the ACL.
	*
	* @param String $role Name of the new role
	* @param Array $parents A non-associative array containing roles
	* @param Bool $master Grants this role access to every action
	* @author Daniel Wilhelm II Murdoch <wilhelm.murdoch@gmail.com>
	* @since v 1.0.0
	* @access Public
	* @return ACL Instance (for chainable calls)
	*/
	public function addRole($role, $parents = array(), $master = false)
	{
		if($this->roleExists($role))
		{
			throw new CoreException("Role '{$role}' already exists within the ACL. You must remove this role before redeclaring it.");
		}

		if($parents)
		{
			$parents = array_unique($parents);

			foreach($parents as $parent)
			{
				if($parent == $role)
				{
					throw new CoreException("Role '{$role}' cannot be a parent of itself.");
				}
				else if(false == trim($parent))
				{
					throw new CoreException("An malformed role has been discovered and it cannot be assigned as parent to role '{$role}'.");
				}
			}
		}

		$this->roles[$role] = array('name' => $role, 'parents' => $parents);

		if($master)
		{
			$this->roles[$role]['master'] = true;
		}

		return $this;
	}


   // ! Mutator Method

   /**
	* Binds a new parent role to an existing role
	*
	* @param String $role Name of the role to assign a parent to
	* @param String $parent Name of the parent role
	* @author Daniel Wilhelm II Murdoch <wilhelm.murdoch@gmail.com>
	* @since v 1.0.0
	* @access Public
	* @return ACL Instance (for chainable calls)
	*/
	public function addParent($role, $parent)
	{
		if(false == $this->roleExists($role))
		{
			throw new CoreException("Role '{$role}' does not exist within the ACL. Add it before assigning it parents.");
		}

		if(false == $this->roleExists($parent))
		{
			throw new CoreException("Role '{$role}' does not exist within the ACL.");
		}

		if($this->parentExists($role, $parent))
		{
			throw new CoreException("Parent '{$parent}' is already assigned to role '{$role}'.");
		}

		$this->roles[$role]['parents'][] = $parent;

		return $this;
	}


   // ! Mutator Method

   /**
	* A convenience method that allows for multiple bindings of parents	and roles.
	*
	* @param Array $data An associative array that includes roles and parents in 'role' => 'parent' format.
	* @author Daniel Wilhelm II Murdoch <wilhelm.murdoch@gmail.com>
	* @since v 1.0.0
	* @access Public
	* @return ACL Instance (for chainable calls)
	*/
	public function addParents($data = array())
	{
		foreach($data as $role => $parent)
		{
			$this->addParent($role, $parent);
		}

		return $this;
	}


   // ! Mutator Method

   /**
	* Unbinds a parent from a role.
	*
	* @param String $role Name of the role to assign a parent to
	* @param String $parent Name of the parent role
	* @author Daniel Wilhelm II Murdoch <wilhelm.murdoch@gmail.com>
	* @since v 1.0.0
	* @access Public
	* @return ACL Instance (for chainable calls)
	*/
	public function removeParent($role, $parent)
	{
		if(false == $this->roleExists($role))
		{
			throw new CoreException("Role '{$role}' does not exist within the ACL. Add it before removing it parents.");
		}

		if(false == $this->roleExists($parent))
		{
			throw new CoreException("Parent '{$parent}' does not exist within the ACL.");
		}

		if(false == $this->parentExists($role, $parent))
		{
			throw new CoreException("Could not remove parent '{$parent}' from role '{$role}'. It is not assigned to this role.");
		}

		$temp_parents = array();

		foreach($this->roles[$role]['parents'] as $roles)
		{
			if($roles != $parent)
			{
				$temp_parents[] = $roles;
			}
		}

		$this->roles[$role]['parents'] = $temp_parents;

		return $this;
	}


   // ! Mutator Method

   /**
	* A convenience method that allows for multiple unbindings of parents and roles.
	*
	* @param Array $data An associative array that includes roles and parents in 'role' => 'parent' format.
	* @author Daniel Wilhelm II Murdoch <wilhelm.murdoch@gmail.com>
	* @since v 1.0.0
	* @access Public
	* @return ACL Instance (for chainable calls)
	*/
	public function removeParents($data = array())
	{
		foreach($data as $role => $parent)
		{
			$this->removeParent($role, $parent);
		}

		return $this;
	}


   // ! Mutator Method

   /**
	* Completely removes a role and all of its 'parent' bindings
	*
	* @param String $role Name of the role to remove
	* @author Daniel Wilhelm II Murdoch <wilhelm.murdoch@gmail.com>
	* @since v 1.0.0
	* @access Public
	* @return ACL Instance (for chainable calls)
	*/
	public function removeRole($role)
	{
		if(false == $this->roleExists($role))
		{
			throw new CoreException("Role '{$role}' does not exist within the ACL. Add it before removing it.");
		}

		unset($this->roles[$role]);

		$temp_roles = array();

		foreach($this->roles as $roles)
		{
			$temp_parents = array();

			if($roles['parents'])
			{
				foreach($roles['parents'] as $parent)
				{
					if($parent != $role)
					{
						$temp_parents[] = $parent;
					}
				}
			}

			$temp_roles[$roles['name']] = array('name' => $roles['name'], 'parents' => $temp_parents);
		}

		$this->roles = $temp_roles;

		return $this;
	}


   // ! Mutator Method

   /**
	* A convenience method that allows for multiple removals of roles.
	*
	* @param Array $roles An array that includes a list of roles to remove.
	* @author Daniel Wilhelm II Murdoch <wilhelm.murdoch@gmail.com>
	* @since v 1.0.0
	* @access Public
	* @return ACL Instance (for chainable calls)
	*/
	public function removeRoles($roles)
	{
		foreach($roles as $role)
		{
			$this->removeRole($role);
		}

		return $this;
	}


   // ! Accessor Method

   /**
	* Checks if a parent has been bound to specified role.
	*
	* @param String $role Name of the role the parent is assigned to
	* @param String $parent Name of the parent role
	* @author Daniel Wilhelm II Murdoch <wilhelm.murdoch@gmail.com>
	* @since v 1.0.0
	* @access Public
	* @return Bool
	*/
	public function parentExists($role, $parent)
	{
		if(false == $this->roleExists($role))
		{
			throw new CoreException("Role '{$role}' does not exist within the ACL. Add it before assigning it parents.");
		}

		return in_array($parent, $this->roles[$role]['parents']);
	}


   // ! Accessor Method

   /**
	* Checks if specified role exists.
	*
	* @param String $role Name of the role to check
	* @author Daniel Wilhelm II Murdoch <wilhelm.murdoch@gmail.com>
	* @since v 1.0.0
	* @access Public
	* @return Bool
	*/
	public function roleExists($role)
	{
		foreach($this->roles as $item)
		{
			if($role == $item['name'])
			{
				return true;
			}
		}

		return false;
	}


   // ! Mutator Method

   /**
	* Adds a new action.
	*
	* @param String $action Name of the new action.
	* @author Daniel Wilhelm II Murdoch <wilhelm.murdoch@gmail.com>
	* @since v 1.0.0
	* @access Public
	* @return ACL Instance (for chainable calls)
	*/
	public function addAction($action)
	{
		if($this->actionExists($action))
		{
			throw new CoreException("Action '{$action}' already exists within the ACL. You cannot declare duplicate actions.");
		}

		$this->actions[$action] = array();

		return $this;
	}


   // ! Mutator Method

   /**
	* A convenience method that allows for the addition of multiple actions.
	*
	* @param Array $actions An array that includes a list of actions to add..
	* @author Daniel Wilhelm II Murdoch <wilhelm.murdoch@gmail.com>
	* @since v 1.0.0
	* @access Public
	* @return ACL Instance (for chainable calls)
	*/
	public function addActions($actions)
	{
		foreach($actions as $action)
		{
			$this->addAction($action);
		}

		return $this;
	}


   // ! Accessor Method

   /**
	* Checks the existance of a specified action.
	*
	* @param String $action Name of the action to check
	* @author Daniel Wilhelm II Murdoch <wilhelm.murdoch@gmail.com>
	* @since v 1.0.0
	* @access Public
	* @return Bool
	*/
	public function actionExists($action)
	{
		return isset($this->actions[$action]);
	}


   // ! Mutator Method

   /**
	* Removes a previously defined action.
	*
	* @param String $action Name of the action to remove
	* @author Daniel Wilhelm II Murdoch <wilhelm.murdoch@gmail.com>
	* @since v 1.0.0
	* @access Public
	* @return ACL Instance (for chainable calls)
	*/
	public function removeAction($action)
	{
		if(false == $this->actionExists($action))
		{
			throw new CoreException("Action '{$action}' does not exist within the ACL.");
		}

		unset($this->actions[$action]);

		return $this;
	}


   // ! Mutator Method

   /**
	* A convenience method that allows for the removal of multiple actions.
	*
	* @param Array $actions An array that includes a list of actions to remove..
	* @author Daniel Wilhelm II Murdoch <wilhelm.murdoch@gmail.com>
	* @since v 1.0.0
	* @access Public
	* @return ACL Instance (for chainable calls)
	*/
	public function removeActions($actions)
	{
		foreach($actions as $action)
		{
			$this->removeAction($action);
		}

		return $this;
	}


   // ! Mutator Method

   /**
	* Grant a role access to an action. If $action is an asterisk (*), the
	* specified $role is GRANTED access to ALL currently defined actions.
	*
	* @param String $role Name of the binding role
	* @param String $action Name of the action to bind
	* @author Daniel Wilhelm II Murdoch <wilhelm.murdoch@gmail.com>
	* @since v 1.0.0
	* @access Public
	* @return ACL Instance (for chainable calls)
	*/
	public function allow($role, $action)
	{
		if(false == $this->roleExists($role))
		{
			throw new CoreException("Role '{$role}' does not exist within the ACL. Add it before binding it to an action.");
		}

		if($action != '*' && false == $this->actionExists($action))
		{
			throw new CoreException("Action '{$action}' does not exist within the ACL. Add it before assigning roles.");
		}

		if($action == '*')
		{
			foreach($this->actions as $current_action => $roles)
			{
				$this->allow($role, $current_action);
			}
		}
		else
		{
			$this->actions[$action][$role] = true;
		}

		return $this;
	}


   // ! Mutator Method

   /**
	* Deny a role access to an action. If $action is an asterisk (*), the
	* specified $role is DENIED access to ALL currently defined actions.
	*
	* @param String $role Name of the binding role
	* @param String $action Name of the action to bind
	* @author Daniel Wilhelm II Murdoch <wilhelm.murdoch@gmail.com>
	* @since v 1.0.0
	* @access Public
	* @return ACL Instance (for chainable calls)
	*/
	public function deny($role, $action)
	{
		if(false == $this->roleExists($role))
		{
			throw new CoreException("Role '{$role}' does not exist within the ACL. Add it before binding it to an action.");
		}

		if($action != '*' && false == $this->actionExists($action))
		{
			throw new CoreException("Action '{$action}' does not exist within the ACL. Add it before removing roles.");
		}

		if($action == '*')
		{
			foreach($this->actions as $current_action => $roles)
			{
				$this->deny($role, $current_action);
			}
		}
		else
		{
			unset($this->actions[$action][$role]);
		}

		return $this;
	}


   // ! Accessor Method

   /**
	* Checks if a role is bound to an action.
	*
	* @param String $role Name of the binding role
	* @param String $action Name of the action to bind
	* @author Daniel Wilhelm II Murdoch <wilhelm.murdoch@gmail.com>
	* @since v 1.0.0
	* @access Public
	* @return Bool
	*/
	public function isRoleBoundToAction($role, $action)
	{
		return isset($this->actions[$action][$role]);
	}


   // ! Accessor Method

   /**
	* This method will recursively iterate up the path of inheritance for
	* the specified $role to determine whether it has been granted access
	* to $action. Yes overides all.
	*
	* @param String $role Name of the binding role
	* @param String $action Name of the action to bind
	* @author Daniel Wilhelm II Murdoch <wilhelm.murdoch@gmail.com>
	* @since v 1.0.0
	* @access Public
	* @return Bool
	*/
	public function isAllowed($role, $action)
	{
		if(false == $this->roleExists($role))
		{
			throw new CoreException("Role '{$role}' does not exist within the ACL.");
		}

		if(false == $this->actionExists($action))
		{
			throw new CoreException("Action '{$action}' does not exist within the ACL.");
		}

		$allowed = false;

		if(false == $this->isRoleBoundToAction($role, $action))
		{
			return false;
		}

		if($this->actions[$action][$role])
		{
			$allowed = true;
		}

		if($this->roles[$role]['master'])
		{
			return true;
		}

		foreach($this->roles[$role]['parents'] as $parent)
		{
			if($this->roles[$parent]['master'])
			{
				return true;
			}

			if($this->roles[$parent]['parents'])
			{
				foreach($this->roles[$parent]['parents'] as $parent_parent)
				{
					$allowed = $this->isAllowed($parent_parent, $action);
				}
			}

			$allowed = $this->isAllowed($parent, $action);
		}

		return $allowed;
	}
}

?>